package org.lestr.secuel.m3ds.sauvegarde;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

import org.lestr.secuel.m3ds.Camera;
import org.lestr.secuel.m3ds.Univers;
import org.lestr.secuel.m3ds.Valeur;
import org.lestr.secuel.m3ds.Variable;
import org.lestr.secuel.m3ds.animations.AnimationObjets3D;
import org.lestr.secuel.m3ds.animations.Cle;
import org.lestr.secuel.m3ds.animations.CleObjet3D;
import org.lestr.secuel.m3ds.lumieres.Ampoule;
import org.lestr.secuel.m3ds.lumieres.Lumiere;
import org.lestr.secuel.m3ds.lumieres.LumiereAmbiante;
import org.lestr.secuel.m3ds.lumieres.Spot;
import org.lestr.secuel.m3ds.maths.IPoint2d;
import org.lestr.secuel.m3ds.maths.ICoordonnees3d;
import org.lestr.secuel.m3ds.objets2D.IImage;
import org.lestr.secuel.m3ds.objets3D.Materiau;
import org.lestr.secuel.m3ds.objets3D.IObjet3d;
import org.lestr.secuel.m3ds.objets3D.IApparence;
import org.lestr.secuel.m3ds.objets3D.ITriangle;

public class M3DSEnregistreur {


    // Définition de constantes
    private static final Namespace espaceDeNommage = Namespace.getNamespace("m3ds", "secuel::m3ds");


    /** 
     * Enregistre un objet 3D dans un fichier.
     * @param objet3D Objet 3D à enregistrer
     * @param fichier Fichier cible
     * @param compression taux de compression (de 0 à 9)
     * @throws M3DSEnregistrementException 
     */
    public static void enregistrer(IObjet3d objet,
                                   File fichier,
                                   int compression) throws M3DSEnregistrementException {

        enregistrer(new M3DSEnregistreur().compilerObjet3D(objet), fichier, compression);

    }// FIN enregistrer


    /** 
     * Enregistre un univers dans un fichier.
     * @param univers Univers à enregistrer
     * @param fichier Fichier cible
     * @param compression taux de compression (de 0 à 9)
     * @throws M3DSEnregistrementException 
     */
    public static void enregistrer(Univers univers,
                                   File fichier,
                                   int compression) throws M3DSEnregistrementException {

        enregistrer(new M3DSEnregistreur().compilerUnivers(univers), fichier, compression);

    }// FIN enregistrer


    /** 
     * Enregistre un élément XML M3DS dans un fichier
     * @param élémentXML Elément à enregistrer
     * @param fichier Fichier cible
     * @param compression taux de compression (de 0 à 9)
     * @throws M3DSEnregistrementException 
     */
    private static void enregistrer(Element élémentXML,
                                    File fichier,
                                    int compression) throws M3DSEnregistrementException {

        // Initialise la sauvegarde
        Document documentXML = new Document();

        // Compile le jeu
        documentXML.setRootElement(élémentXML);

        // Mémorise le moteur et sa version
        documentXML.getRootElement().setAttribute(new Attribute("moteur", "M3DS", espaceDeNommage));
        documentXML.getRootElement().setAttribute(new Attribute("version", "1.0", espaceDeNommage));

        // Enregistre dans le fichier
        try {

            // Format classique
            XMLOutputter sortie = new XMLOutputter(Format.getPrettyFormat());

            // Compresser le flux
            ZipOutputStream flux = new ZipOutputStream(new FileOutputStream(fichier));
            flux.setLevel(compression);
            flux.putNextEntry(new ZipEntry("Secuel"));

            // On enregistre
            sortie.output(documentXML, flux);

            // Ferme le flux compressé
            flux.closeEntry();
            flux.close();

        } catch (java.io.IOException e) {
        }

    }// FIN enregistrer


    // Déclaration des attributs
    private Hashtable<ITriangle, String> idsTriangles;


    private int dernierID;


    /** Constructeur */
    public M3DSEnregistreur() {

        // Instanciation des attributs
        idsTriangles = null;
        dernierID = 0;

    }// FIN Constructeur


    /** Compile un univers */
    public Element compilerUnivers(Univers univers) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("univers", espaceDeNommage);

        // Mémorise les paramètres généraux
        rslt.setAttribute(new Attribute("nom", univers.getNom()));

        // Parcours les objets 3D de l'univers et les mémorises
        for (IObjet3d objet3D : univers.getObjets3D())
            // Mémorise
            rslt.addContent(compilerObjet3D(objet3D));

        // Parcours les lumières de l'univers et les mémorises
        for (Lumiere lumière : univers.getLumières())
            rslt.addContent(compilerLumière(lumière));

        // Parcours les caméras de l'univers et les mémorises
        for (Camera caméra : univers.getCaméras())
            rslt.addContent(compilerCaméra(caméra));

        // TODO

        // Parcours et mémorise les variables
        for (Variable variable : univers.getVariables())
            rslt.addContent(compilerVariable(variable));

        // Retourne le résultat
        return rslt;

    }// FIN compilerUnivers


    /** Compile une caméra */
    public Element compilerCaméra(Camera caméra) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("caméra", espaceDeNommage);

        // Mémorise les paramètres généraux de la lumière
        rslt.setAttribute(new Attribute("nom", caméra.getNom(), espaceDeNommage));
        rslt.setAttribute(new Attribute("x", "" + caméra.getX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + caméra.getY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("z", "" + caméra.getZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleX", "" + caméra.getAngleX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleY", "" + caméra.getAngleY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleZ", "" + caméra.getAngleZ(), espaceDeNommage));

        // Retourne le résultat
        return rslt;

    }// FIN compilerLumière


    /** Compile une lumière */
    public Element compilerLumière(Lumiere lumière) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("lumière", espaceDeNommage);

        // Mémorise les paramètres généraux de la lumière
        rslt.setAttribute(new Attribute("nom", lumière.getNom(), espaceDeNommage));
        rslt.setAttribute(new Attribute("x", "" + lumière.getX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + lumière.getY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("z", "" + lumière.getZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("intensité", "" + lumière.getIntensité(), espaceDeNommage));
        rslt.setAttribute(new Attribute("estAllumé", "" + lumière.isAllumée(), espaceDeNommage));
        rslt.setAttribute(new Attribute("rouge", "" + lumière.getRouge(), espaceDeNommage));
        rslt.setAttribute(new Attribute("vert", "" + lumière.getVert(), espaceDeNommage));
        rslt.setAttribute(new Attribute("bleu", "" + lumière.getBleu(), espaceDeNommage));

        // Parcours et mémorise les variables
        for (Variable variable : lumière.getVariables())
            rslt.addContent(compilerVariable(variable));

        // Si c'est une lumière ambiante
        if (lumière instanceof LumiereAmbiante)
            // Mémorise les paramètres spécifiques au lumières ambiantes
            rslt.setAttribute(new Attribute("type", "ambiante", espaceDeNommage));

        // Si c'est une ampoule
        if (lumière instanceof Ampoule) {

            // Mémorise les paramètres spécifiques au lumières ambiantes
            rslt.setAttribute(new Attribute("type", "ampoule", espaceDeNommage));
            rslt.setAttribute(new Attribute("portée", "" + ((Ampoule) lumière).getPortée(), espaceDeNommage));

        }

        // Si c'est un spot
        if (lumière instanceof Spot) {

            // Mémorise les paramètres spécifiques au lumières ambiantes
            rslt.setAttribute(new Attribute("type", "spot", espaceDeNommage));
            rslt.setAttribute(new Attribute("portée", "" + ((Spot) lumière).getPortée(), espaceDeNommage));
            rslt.setAttribute(new Attribute("angle", "" + ((Spot) lumière).getAngle(), espaceDeNommage));
            rslt.setAttribute(new Attribute("angleX", "" + ((Spot) lumière).getAngleX(), espaceDeNommage));
            rslt.setAttribute(new Attribute("angleY", "" + ((Spot) lumière).getAngleY(), espaceDeNommage));
            rslt.setAttribute(new Attribute("angleZ", "" + ((Spot) lumière).getAngleZ(), espaceDeNommage));

        }

        // Retourne le résultat
        return rslt;

    }// FIN compilerLumière


    /** Compile un objet3D */
    public Element compilerObjet3D(IObjet3d objet3D) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("objet3D", espaceDeNommage);

        // Mémorise les paramètres généraux de l'objet 3D
        rslt.setAttribute(new Attribute("nom", objet3D.obtenirNom(), espaceDeNommage));
        rslt.setAttribute(new Attribute("x", "" + objet3D.getX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + objet3D.getY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("z", "" + objet3D.getZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleX", "" + objet3D.getAngleX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleY", "" + objet3D.getAngleY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleZ", "" + objet3D.getAngleZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionX", "" + objet3D.getDimensionX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionY", "" + objet3D.getDimensionY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionZ", "" + objet3D.getDimensionZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("transparence", "" + objet3D.obtenirTransparence(), espaceDeNommage));
        rslt.setAttribute(new Attribute("réflexion", "" + objet3D.obtenirRéflexion(), espaceDeNommage));
        rslt.setAttribute(new Attribute("solide", "" + objet3D.estSolide(), espaceDeNommage));
        rslt.setAttribute(new Attribute("fichierExclusif", "" + objet3D.isFichierExclusif(), espaceDeNommage));

        // Si l'objet est une forme fichier exclusive
        if (objet3D.isFichierExclusif())
            // Mémorise l'adresse du fichier
            try {
                rslt.setAttribute(new Attribute("fichier", "" + objet3D.getFichier().getCanonicalPath(), espaceDeNommage));
            } catch (IOException e) {
            }
        // Sinon
        else {

            // Initialise les IDs de triangle
            idsTriangles = new Hashtable<ITriangle, String>();
            dernierID = 0;

            // Parcours et mémorise les triangles de l'objets
            Element trianglesXML = new Element("triangles", espaceDeNommage);
            rslt.addContent(trianglesXML);
            for (ITriangle triangle : objet3D.obtenirTriangles())
                trianglesXML.addContent(compilerTriangle(triangle));

            // Mémorise la texture de l'objet
            rslt.addContent(compilerTexture(objet3D.obtenirApparence()));

        }

        // Parcours et mémorise les animations de l'objet
        if (objet3D.getAnimations() != null) {
            Element animationsXML = new Element("animations", espaceDeNommage);
            rslt.addContent(animationsXML);
            for (AnimationObjets3D animation : objet3D.getAnimations())
                animationsXML.addContent(compilerAnimationObjet3D(animation));
        }

        // Parcours et mémorise les variables
        for (Variable variable : objet3D.obtenirVariables())
            rslt.addContent(compilerVariable(variable));

        // Retourne le résultat
        return rslt;

    }// FIN compilerObjet3D


    /** Compile une texture */
    public Element compilerTexture(IApparence texture) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("texture", espaceDeNommage);

        // Mémorise les paramètres de la texture
        rslt.setAttribute(new Attribute("x", "" + texture.getPosition(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + texture.getY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionX", "" + texture.getDimension(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionY", "" + texture.getDimensionY(), espaceDeNommage));
        rslt.addContent(compilerMatériau(texture.obtenirMatériauStandard()));
        rslt.setAttribute(new Attribute("autoNormales", "" + texture.sontNormalesAutomatiques(), espaceDeNommage));
        rslt.setAttribute(new Attribute("autoTexturé", "" + texture.estTexturageAutomatique(), espaceDeNommage));

        // Coordonnées de texture
        if (!texture.estTexturageAutomatique()) {

            Element coordonnées = new Element("coordonnées", espaceDeNommage);
            for (IPoint2d point2D : texture.obtenirCoordonnées())
                coordonnées.addContent(compilerPoint2D(point2D));
            rslt.addContent(coordonnées);

        }

        // Normales
        if (!texture.sontNormalesAutomatiques()) {

            Element normales = new Element("normales", espaceDeNommage);
            for (ICoordonnees3d point3D : texture.obtenirNormales())
                normales.addContent(compilerPoint3D(point3D));
            rslt.addContent(normales);

        }

        // Compile les matériaux spécifiques
        for (ITriangle triangle : texture.obtenirObjet3d().obtenirTriangles())
            if (texture.obtenirMatériau(triangle) != null) {
                Element matériauSpécifique = new Element("matériauSpécifique", espaceDeNommage);
                matériauSpécifique.setAttribute("triangleID", idsTriangles.get(triangle), espaceDeNommage);
                matériauSpécifique.addContent(compilerMatériau(texture.obtenirMatériau(triangle)));
                rslt.addContent(matériauSpécifique);
            }

        // Retourne le résultat
        return rslt;

    }// FIN compilerTexture


    /** Compile un matériau */
    public Element compilerMatériau(Materiau matériau) {

        // Variables locales
        Element rslt = new Element("matériau", espaceDeNommage);

        // Mémorise les paramètres du matériau
        rslt.setAttribute(new Attribute("rougeAmbiant", "" + matériau.obtenirRougeAmbiant(), espaceDeNommage));
        rslt.setAttribute(new Attribute("vertAmbiant", "" + matériau.obtenirVertAmbiant(), espaceDeNommage));
        rslt.setAttribute(new Attribute("bleuAmbiant", "" + matériau.obtenirBleuAmbiant(), espaceDeNommage));
        rslt.setAttribute(new Attribute("rougeDiffue", "" + matériau.obtenirRougeDiffue(), espaceDeNommage));
        rslt.setAttribute(new Attribute("vertDiffue", "" + matériau.obtenirVertDiffue(), espaceDeNommage));
        rslt.setAttribute(new Attribute("bleuDiffue", "" + matériau.obtenirBleuDiffue(), espaceDeNommage));
        rslt.setAttribute(new Attribute("rougeSpéculaire", "" + matériau.obtenirRougeSpéculaire(), espaceDeNommage));
        rslt.setAttribute(new Attribute("vertSpéculaire", "" + matériau.obtenirVertSpéculaire(), espaceDeNommage));
        rslt.setAttribute(new Attribute("bleuSpéculaire", "" + matériau.obtenirBleuSpéculaire(), espaceDeNommage));
        rslt.setAttribute(new Attribute("rougeEmissif", "" + matériau.obtenirRougeEmissif(), espaceDeNommage));
        rslt.setAttribute(new Attribute("vertEmissif", "" + matériau.obtenirVertEmissif(), espaceDeNommage));
        rslt.setAttribute(new Attribute("bleuEmissif", "" + matériau.obtenirBleuEmissif(), espaceDeNommage));

        // Mémorise l'image si elle est présent
        if (matériau.obtenirImage() != null)
            rslt.addContent(compilerImage(matériau.obtenirImage()));

        // Retourne le résultat
        return rslt;

    }// FIN compilerMatériau


    /** Compile une image */
    public Element compilerImage(IImage image) {

        // Variables locales
        Element rslt = new Element("image", espaceDeNommage);

        // Mémorise les paramètres de l'image
        rslt.setAttribute(new Attribute("format", "M3DS", espaceDeNommage));
        rslt.setAttribute(new Attribute("largeur", "" + image.obtenirLargeur(), espaceDeNommage));
        rslt.setAttribute(new Attribute("hauteur", "" + image.obtenirHauteur(), espaceDeNommage));

        // Mémorise les pixels
        String pixels = "";
        for (int i = 0; i <= image.obtenirLargeur() - 1; i = i + 1) {
            for (int j = 0; j <= image.obtenirHauteur() - 1; j = j + 1)
                pixels = pixels + image.obtenirPixel(i, j).getRouge() + "\t"
                         + image.obtenirPixel(i, j).getVert() + "\t"
                         + image.obtenirPixel(i, j).getBleu() + "\t"
                         + ((int) (image.obtenirTransparence(i, j) * 255)) + "\t";
            pixels = pixels + "\n";
        }
        rslt.setText(pixels);

        // Retourne le résultat
        return rslt;

    }// FIN compilerImage


    /** Compile un triangle */
    public Element compilerTriangle(ITriangle triangle) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("triangle", espaceDeNommage);

        // Attribut un ID au triangle
        dernierID = dernierID + 1;
        rslt.setAttribute("ID", "" + dernierID, espaceDeNommage);
        idsTriangles.put(triangle, "" + dernierID);

        // Mémorise les sommets
        rslt.addContent(compilerPoint3D(triangle.obtenirSommet1()));
        rslt.addContent(compilerPoint3D(triangle.obtenirSommet2()));
        rslt.addContent(compilerPoint3D(triangle.obtenirSommet3()));

        // Retourne le résultat
        return rslt;

    }// FIN compilerTriangle


    /** Compile un point3D */
    public Element compilerPoint3D(ICoordonnees3d point3D) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("point3D", espaceDeNommage);

        // Mémorise les coordonnées du point
        rslt.setAttribute(new Attribute("x", "" + point3D.obtenirX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + point3D.obtenirY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("z", "" + point3D.obtenirZ(), espaceDeNommage));

        // Retourne le résultat
        return rslt;

    }// FIN compilerPoint3D


    /** Compile un point2D */
    public Element compilerPoint2D(IPoint2d point2D) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("point2D", espaceDeNommage);

        // Mémorise les coordonnées du point
        rslt.setAttribute(new Attribute("x", "" + point2D.obtenirX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + point2D.obtenirY(), espaceDeNommage));

        // Retourne le résultat
        return rslt;

    }// FIN compilerPoint2D


    /** Compile une animation */
    public Element compilerAnimationObjet3D(AnimationObjets3D animation) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("animation", espaceDeNommage);

        // Mémorise le nom de l'animation
        rslt.setAttribute(new Attribute("nom", animation.getNom(), espaceDeNommage));

        // Parcours et mémorise les clés
        for (Cle clé : animation.getClés())
            rslt.addContent(compilerCléObjet3D((CleObjet3D) clé));

        // Parcours et mémorise les variables
        for (Variable variable : animation.getVariables())
            rslt.addContent(compilerVariable(variable));

        // Retourne le résultat
        return rslt;

    }// FIN compilerAnimationObjet3D


    /** Compile une variable */
    public Element compilerVariable(Variable variable) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("variable", espaceDeNommage);

        // Mémorise le nom de la variable
        rslt.setAttribute(new Attribute("nom", variable.getNom(), espaceDeNommage));

        // Mémorise sa valeur si définie
        if (variable.getValeur() != null)
            rslt.addContent(compilerValeur(variable.getValeur()));

        // Retourne le résultat
        return rslt;

    }// FIN compilerVariable


    /** Compile une valeur */
    public Element compilerValeur(Valeur valeur) throws M3DSEnregistrementException {

        // Valeurs locales
        Element rslt = new Element("valeur", espaceDeNommage);

        // Mémorise le type de la valeur
        rslt.setAttribute(new Attribute("type", "" + valeur.getType(), espaceDeNommage));

        // Mémorise ses données
        switch (valeur.getType()) {

            case Valeur.DOUBLE:
                rslt.addContent("" + valeur.getDouble());
                break;

            case Valeur.FLOAT:
                rslt.addContent("" + valeur.getFloat());
                break;

            case Valeur.INT:
                rslt.addContent("" + valeur.getInt());
                break;

            case Valeur.STRING:
                rslt.addContent(valeur.getString());
                break;

        }

        // Retourne le résultat
        return rslt;

    }// FIN compilerValeur


    /** Compile une clé */
    public Element compilerCléObjet3D(CleObjet3D clé) throws M3DSEnregistrementException {

        // Variables locales
        Element rslt = new Element("clé", espaceDeNommage);

        // Mémorise les paramètres de la clé
        rslt.setAttribute(new Attribute("x", "" + clé.getX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("y", "" + clé.getY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("z", "" + clé.getZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleX", "" + clé.getAngleX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleY", "" + clé.getAngleY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("angleZ", "" + clé.getAngleZ(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionX", "" + clé.getDimensionX(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionY", "" + clé.getDimensionY(), espaceDeNommage));
        rslt.setAttribute(new Attribute("dimensionZ", "" + clé.getDimensionZ(), espaceDeNommage));

        // Retourne le résultat
        return rslt;

    }// FIN compilerClé


}// FIN Classe
